﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using Antlr.Runtime;

namespace VSDoc2JsDoc
{
    class JsDefinition
    {
        public IToken StartToken { get; set; }
        public IToken EndToken { get; set; }
    }

    [System.Diagnostics.DebuggerDisplay("ClassName = {ClassName}")]
    class ClassDefinition : JsDefinition
    {
        public string ClassName { get; set; }
        public string BaseClassName { get; set; }

        public IToken DefinitionStart { get; set; }

        public List<MemberDefinition> MemberDefinitions { get; private set; }

        public ClassDefinition()
        {
            ClassName = "<missing>";
            MemberDefinitions = new List<MemberDefinition>();
        }
    }

    [System.Diagnostics.DebuggerDisplay("MemberName = {MemberName}")]
    class MemberDefinition : JsDefinition
    {
        public ClassDefinition ClassDefinition { get; set; }
        public string MemberName { get; set; }
        public XElement VsDoc { get; set; }
        public string JsDoc { get; set; }
        public bool IsFunction { get; set; }

        public bool IsConstructor 
        {
            get 
            {
                return MemberName == "constructor";
            }
        }

        public MemberDefinition()
        {
            MemberName = "<missing>";
            VsDoc = XElement.Parse("<doc startChar=\"0\" length=\"0\"></doc>");
            JsDoc = String.Empty;
        }
    }

    class JsScript
    {
        public List<ClassDefinition> ClassDefinitions { get; private set; }
        public string RawText { get; set; }

        public JsScript()
        {
            ClassDefinitions = new List<ClassDefinition>();
        }
    }


    class JsParser
    {
        private ITokenStream tokenStream;
        private int tokenCount;

        public JsScript Parse(string text)
        {
            JsScript script = new JsScript();
            script.RawText = text;

            var lexer = new ES3Lexer(new ANTLRStringStream(text));
            var parser = new ES3Parser(new CommonTokenStream(lexer)) { DebugMode = true };
            tokenStream = parser.TokenStream;
            ReadAllTokens();

            script.ClassDefinitions.AddRange(ReadClasses(0, tokenCount));

            return script;
        }


        private List<ClassDefinition> ReadClasses(int start, int end)
        {
            List<ClassDefinition> classDefs = new List<ClassDefinition>();

            while (0 <= start && start < end)
            {
                ClassDefinition classDef = ReadClassBlock(start);
                if (classDef == null)
                {
                    start++;
                    continue;
                }
                else
                {
                    classDefs.Add(classDef);
                    start = classDef.EndToken.TokenIndex + 1;
                }
            }

            return classDefs;
        }

        private ClassDefinition ReadClassBlock(int start)
        {
            ClassDefinition classDef = FindClassDefinition(start);

            if (classDef == null)
            {
                return null;
            }

            start = classDef.StartToken.TokenIndex;
            while (start <= classDef.EndToken.TokenIndex)
            {
                MemberDefinition memberDef = ReadMemberBlock(start);
                if (memberDef == null)
                {
                    start++;
                    continue;
                }
                else
                {
                    classDef.MemberDefinitions.Add(memberDef);
                    memberDef.ClassDefinition = classDef;
                    start = memberDef.EndToken.TokenIndex + 1;
                }
            }

            return classDef;
        }

        private MemberDefinition ReadMemberBlock(int start)
        {
            int memberIndex = Find(ES3Parser.COLON, start: start, currentScope: true);
            if (memberIndex < 0)
            {
                return null;
            }

            MemberDefinition memberDef = new MemberDefinition();

            IToken memberName = tokenStream.Get(memberIndex - 1);
            IToken value = tokenStream.Get(memberIndex + 1);

            if (memberName.Type == ES3Parser.WhiteSpace)
            {
                memberName = tokenStream.Get(memberIndex - 2);
            }

            if (value.Type == ES3Parser.WhiteSpace)
            {
                value = tokenStream.Get(memberIndex + 2);
            }

            memberDef.MemberName = memberName.Text;
            memberDef.StartToken = memberName;

            int endIndex = Find(ES3Parser.COMMA, start: memberIndex, currentScope: true);

            if (endIndex < 0)
            {
                int parent = FindParentBlock(start);
                endIndex = FindClosingBrace(parent);
            }

            memberDef.EndToken = tokenStream.Get(endIndex);

            memberDef.IsFunction = value.Type == ES3Parser.FUNCTION;
            if (memberDef.IsFunction)
            {
                StringBuilder doc = new StringBuilder();
                int bodyIndex = Find(ES3Parser.LBRACE, start: memberDef.StartToken.TokenIndex, currentScope: true);
                start = FindDocumentationComment(bodyIndex + 1, currentScope: true);
                int docStart = start;
                int docEnd = docStart;

                for (int previousIndex = bodyIndex;
                    0 <= start && start < memberDef.EndToken.TokenIndex && (tokenStream.Get(previousIndex).Line + 1 == tokenStream.Get(start).Line);
                    start = FindDocumentationComment(start + 1, currentScope: true))
                {
                    IToken token = tokenStream.Get(start);
                    doc.AppendLine(token.Text.TrimStart('/'));
                    previousIndex = start;
                    docEnd = start;
                }

                try
                {
                    if (docStart != -1)
                    {
                        memberDef.VsDoc = XElement.Parse("<doc>" + doc + "</doc>");
                        memberDef.VsDoc.SetAttributeValue("startChar", tokenStream.Get(docStart).StartIndex);
                        memberDef.VsDoc.SetAttributeValue("length", tokenStream.Get(docEnd).StopIndex - tokenStream.Get(docStart).StartIndex + 1);
                    }
                }
                catch
                {
                    Console.Error.WriteLine("Invalid XML at line {0}", tokenStream.Get(bodyIndex).Line + 1);
                    throw;
                }
            }

            return memberDef;
        }

        private ClassDefinition FindClassDefinition(int start)
        {
            ClassDefinition cd = null;

            Func<IToken, string> getName = t => 
            {
                if (t.Type == ES3Parser.StringLiteral)
                {
                    return t.Text.Trim('\'', '"');
                }

                StringBuilder sb = new StringBuilder();

                for (; t.Type == ES3Parser.Identifier || t.Type == ES3Parser.DOT; t = tokenStream.Get(t.TokenIndex + 1))
                {
                    sb.Append(t.Text);
                }

                return sb.ToString();
            };

            for (int i = start; 0 <= i && i < tokenCount; i++)
            {
                IToken t0 = tokenStream.Get(i);
                IToken t1 = tokenStream.Get(i + 1);
                IToken t2 = tokenStream.Get(i + 2);
                IToken t3 = tokenStream.Get(i + 3);
                IToken t4 = tokenStream.Get(i + 4);
                IToken t5 = tokenStream.Get(i + 5);
                IToken t6 = tokenStream.Get(i + 6);
                IToken t7 = tokenStream.Get(i + 7);
                IToken t8 = tokenStream.Get(i + 8);
                IToken t9 = tokenStream.Get(i + 9);
                IToken t10 = tokenStream.Get(i + 10);

                // $data.Class.define('class name', 'base class', ...);
                if (t0.Type == ES3Parser.Identifier && t0.Text == "$data" &&
                    t1.Type == ES3Parser.DOT &&
                    t2.Type == ES3Parser.Identifier && t2.Text == "Class" &&
                    t3.Type == ES3Parser.DOT &&
                    t4.Type == ES3Parser.Identifier && t4.Text == "define" &&
                    t5.Type == ES3Parser.LPAREN &&
                    t6.Type == ES3Parser.StringLiteral)
                {
                    cd = new ClassDefinition();
                    cd.ClassName = t6.Text.Trim('\'', '"');
                    cd.BaseClassName = getName(t9);
                    int lastParamIndex = Find(ES3Parser.COMMA, start: t6.TokenIndex, currentScope: true, hitCount: 3);
                    cd.StartToken = tokenStream.Get(Find(ES3Parser.LBRACE, start: lastParamIndex));
                    cd.EndToken = tokenStream.Get(FindClosingBrace(cd.StartToken.TokenIndex));
                    cd.DefinitionStart = t0;

                    break;
                }

                // $C('class name', 'base class')
                if (t0.Type == ES3Parser.Identifier && t0.Text == "$C" &&
                    t1.Type == ES3Parser.LPAREN &&
                    t2.Type == ES3Parser.StringLiteral)
                {
                    cd = new ClassDefinition();
                    cd.ClassName = t2.Text.Trim('\'', '"');
                    cd.BaseClassName = getName(t5);
                    int lastParamIndex = Find(ES3Parser.COMMA, start: t2.TokenIndex, currentScope: true, hitCount: 3);
                    cd.StartToken = tokenStream.Get(Find(ES3Parser.LBRACE, start: lastParamIndex));
                    cd.EndToken = tokenStream.Get(FindClosingBrace(cd.StartToken.TokenIndex));
                    cd.DefinitionStart = t0;

                    break;
                }
                
                // $data.Class.defineEx('class name', [{type: 'base type'}])
                if (t0.Type == ES3Parser.Identifier && t0.Text == "$data" &&
                    t1.Type == ES3Parser.DOT &&
                    t2.Type == ES3Parser.Identifier && t2.Text == "Class" &&
                    t3.Type == ES3Parser.DOT &&
                    t4.Type == ES3Parser.Identifier && t4.Text == "defineEx" &&
                    t5.Type == ES3Parser.LPAREN &&
                    t6.Type == ES3Parser.StringLiteral)
                {
                    cd = new ClassDefinition();
                    cd.ClassName = t6.Text.Trim('\'', '"');

                    // search base type
                    int array = Find(ES3Parser.LBRACK, start: t6.TokenIndex + 1, currentScope: true); // array required
                    int obj = Find(ES3Parser.LBRACE, start: array + 1, currentScope: true);
                    if (obj >= 0)
                    {
                        int typeProp = Find(ES3Parser.Identifier, start: obj + 1, currentScope: true);
                        if (typeProp >= 0)
                        {
                            IToken typep = tokenStream.Get(typeProp);
                            if (typep.Text == "type")
                            {
                                int baseIndex = typeProp + 1;
                                if (tokenStream.Get(baseIndex).Type == ES3Parser.COLON || tokenStream.Get(baseIndex).Type == ES3Parser.WhiteSpace) baseIndex++;
                                if (tokenStream.Get(baseIndex).Type == ES3Parser.COLON || tokenStream.Get(baseIndex).Type == ES3Parser.WhiteSpace) baseIndex++;
                                if (tokenStream.Get(baseIndex).Type == ES3Parser.WhiteSpace) baseIndex++;

                                cd.BaseClassName = getName(tokenStream.Get(baseIndex));
                            }
                        }
                    }

                    int lastParamIndex = Find(ES3Parser.COMMA, start: t6.TokenIndex, currentScope: true, hitCount: 3);
                    cd.StartToken = tokenStream.Get(Find(ES3Parser.LBRACE, start: lastParamIndex));
                    cd.EndToken = tokenStream.Get(FindClosingBrace(cd.StartToken.TokenIndex));
                    cd.DefinitionStart = t0;

                    break;
                }

                // ba.se.Cl.a.ss.extend('class name'...)
                if (t0.Type == ES3Parser.Identifier &&
                    t1.Type == ES3Parser.DOT &&
                    t2.Type == ES3Parser.Identifier && t2.Text == "extend" &&
                    t3.Type == ES3Parser.LPAREN &&
                    t4.Type == ES3Parser.StringLiteral)
                {
                    int j;
                    StringBuilder sb = new StringBuilder();
                    for (j = t0.TokenIndex; j >= 0 && tokenStream.Get(j).Type == ES3Parser.Identifier && tokenStream.Get(j + 1).Type == ES3Parser.DOT; j -= 2)
                    { 
                        sb.Insert(0, "." + tokenStream.Get(j).Text);
                    }
                    
                    cd = new ClassDefinition();
                    cd.ClassName = t4.Text.Trim('\'', '"');
                    cd.BaseClassName = sb.ToString().Substring(1);

                    cd.StartToken = tokenStream.Get(Find(ES3Parser.LBRACE, start: t4.TokenIndex + 1));
                    cd.EndToken = tokenStream.Get(FindClosingBrace(cd.StartToken.TokenIndex));
                    cd.DefinitionStart = t0;

                    break;
                }
            }

            if (cd != null && String.IsNullOrEmpty(cd.BaseClassName))
            {
                cd.BaseClassName = "$data.Base";
            }

            return cd;
        }

        private void Match(int start, int end)
        { 
            
        }

        private int FindParentBlock(int start, int level = 1)
        {
            return FindParentScope(ES3Parser.LBRACE, ES3Parser.RBRACE, start, level);
        }

        private int FindClosingBrace(int openingBrace)
        {
            return FindPair(ES3Parser.LBRACE, ES3Parser.RBRACE, openingBrace);
        }

        private int FindPair(int leftType, int rightType, int start)
        {
            if (0 > start || start >= tokenCount)
            {
                return -1;
            }

            int step = tokenStream.Get(start).Type == leftType ? 1 : -1;

            int deep = 0;
            for (int i = start; 0 <= i && i < tokenCount; i += step)
            {
                IToken token = tokenStream.Get(i);

                if (token.Type == leftType)
                {
                    deep++;
                }
                else if (token.Type == rightType)
                {
                    deep--;
                }

                if (deep == 0)
                {
                    return i;
                }
            }

            return -1;
        }

        private int FindParentScope(int leftType, int rightType, int start, int level = 1)
        {
            int balance = 0;
            for (int i = start - 1; i >= 0; i--)
            {
                IToken token = tokenStream.Get(i);

                if (token.Type == rightType)
                {
                    balance--;
                }
                else if (token.Type == leftType)
                {
                    balance++;
                }

                if (balance > 0)
                {
                    if (level > 1)
                    {
                        return FindParentScope(leftType, rightType, i, level - 1);
                    }
                    else
                    {
                        return i;
                    }
                }
            }

            return -1;
        }

        private int FindDocumentationComment(int start = 0, bool forward = true, bool currentScope = false)
        {
            return Find(ES3Parser.SingleLineComment, t => t.Text.StartsWith("///"), start, forward ? 1 : -1, currentScope);
        }

        private int Find(int tokenType, Predicate<IToken> predicate = null, int start = 0, int step = 1, bool currentScope = false, int hitCount = 1)
        {
            return Find(t => t.Type == tokenType && (predicate == null ? true : predicate(t)), start, step, currentScope, hitCount);
        }

        private int Find(Predicate<IToken> predicate, int start = 0, int step = 1, bool currentScope = false, int hitCount = 1)
        {
            List<int[]> scopes = null;

            if (currentScope)
            {
                scopes = new List<int[]>()
                {
                    new int[] { ES3Parser.LPAREN, ES3Parser.RPAREN, 0 },
                    new int[] { ES3Parser.LBRACE, ES3Parser.RBRACE, 0 },
                    new int[] { ES3Parser.LBRACK, ES3Parser.RBRACK, 0 }
                };
            }

            bool inScope = false;

            for (int i = start; 0 <= i && i < tokenCount; i += step)
            {
                IToken token = tokenStream.Get(i);

                if (!inScope && predicate(token))
                {
                    hitCount--;
                }

                if (hitCount == 0)
                {
                    return i;
                }

                if (currentScope)
                {
                    inScope = false;
                    foreach (var scope in scopes)
                    {
                        if (token.Type == scope[0])
                            scope[2]--;
                        else if (token.Type == scope[1])
                            scope[2]++;

                        if (Math.Sign(scope[2]) == Math.Sign(step))
                            return -1;

                        inScope = inScope || scope[2] != 0;
                    }
                }
            }

            return -1;
        }

        private void ReadAllTokens()
        {
            tokenStream.Consume();
            IToken token = tokenStream.Get(0);
            for (tokenCount = 0; token.Type != ES3Parser.EOF; tokenCount++)
            {
                token = tokenStream.Get(tokenCount);
                tokenStream.Consume();
            }
        }

    }
}
